iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 6
0
Modern Web

JS Design Pattern 系列 第 6

JS Design Pattern Day06-發佈/訂閱模式 Publish/Subscribe(下)

  • 分享至 

  • xImage
  •  

今天第六天,剛好是禮拜天,冷冷的夜晚,睡前看一下Design Pattern,不只暖你的胃更暖你的心。
其實我真的很想把這篇拆成上中下三集,或是1-7像哈利波特一樣,假日寫文章真是要命

繼續把發佈/訂閱模式寫完

我們繼續用實際範例講解發佈訂閱模式,今天我們要做一個網站,登入的時候要依照使用者的資訊做一些畫面更新,例如:更新使用者頭像、更新資訊列表、購物車更新...等事情,比如像這樣寫:

login.success(function (data) {
    head.setAvatar(data);
    nav.setAvatar(data);
    cart.refresh();
    ...
});

然後,今天老闆突然決定登入之後,儀表板畫面也要更新,所以只好找到之前寫的上面那段code再加入一段

dashboard.refresh();

其實像我們公司是以功能來分配給工程師工作,假如你又是負責登入功能的話,現在這件事也許有些地方會困擾你,像是必須去了解每個功能的指令例如head裡的setAvatar,以及我這段登入成功也太多事情了。

所以我們可以用發佈/訂閱模式改造一下:
首先當我們登入成功的時候我們只要發佈一個登入成功的消息就好

$.ajax('http:....', function (data) {
    login.trigger('loginSuccess', data);
});

那剩下的事呢?當然交給個功能開發或是維護的人自己做啦,像這樣:

var header = (function () {
    login.listen('loginSuccess', function (data) { header.setAvatar(data) });
    return {
        setAvatar: function (data) {
            console.log('設置頭像');
        }
    }
})();

又臨時增加需求的話就只要在各自功能增加就可以囉:

var dashboard = (function () {
    login.listen('loginSuccess', function (data) { dashboard.refresh(data) });
    return {
        refresh: function (data) {
            console.log('dashboard refresh');
        }
    }
})();

總結以上(包含上篇)這些寫法可能有些問題,每個發佈者都要添加listen、trigger、clientList這些方法,感覺有點重複有點浪費。我們可以試著做全域的物件:

做法其實跟之前的內容很像,只要先做一個全域的物件,內容回傳實際執行方法:

//做一個全域的物件
var Event = (function () {
    var clientList = {},
        listen,
        trigger,
        remove;
      
    //實際執行方法(內容都跟上述的範例一樣)
    listen = function (key, fn) {
        if (!clientList[key]) {
            clientList[key] = [];
        }
        clientList[key].push(fn);
    };

    trigger = function () {
        var key = Array.prototype.shift.call(arguments);
        var fnPool = clientList[key];
        if (!fnPool || fnPool.length === 0) {
            return false;
        }
        for (var i = 0, fn; fn = fnPool[i++];) {
            fn.apply(this, arguments);
        }
    };
    remove = function (key, removeFn) {
        var fnPool = this.clientList[key];
        if (!fnPool || fnPool.length === 0) {
            return false;
        }
        if (!removeFn) {
            fnPool && (fnPool.length = 0);
        } else {
            for (var i = 0, fn; fn = fnPool[i]; i++) {
                if (fn === removeFn) {
                    fnPool.splice(i, 1);
                }
            }
        }
    };
    //回傳給外面呼叫
    return {
        listen: listen,
        trigger: trigger,
        remove: remove
    }
})();

我們實際用前端網頁來示範如何使用,兩個物件模組可以在保持封裝特性前提下進行通訊:

(function () {
    createButtonA();
    createDivB();

    function createButtonA() {
        var count = 0;
        $('<button>').text('A').appendTo('body').click(function () {
            Event.trigger('add', ++count);
        });
    }

    function createDivB() {
        var b = $('<div>').text('').appendTo('body');
        Event.listen('add', function (count) {
            b.text(count);
        });
    }
})();

那這樣的全域事件用久了一定會出現事件名稱衝突的狀況,所以我們可以給這個Event提供建立命名空間的功能,以及若先發佈再訂閱也要可以收到發佈的訊息:

var Event = (function () {
    //預設命名
    var _default = 'default';
    var Event = function () {
        var _slice = Array.prototype.slice,
            _shift = Array.prototype.shift,
            _unshift = Array.prototype.unshift,
            namespaceCache = {};

        //實作發佈訂閱等內容    
        var _listen = function (key, fn, cache) {
            if (!cache[key]) {
                cache[key] = [];
            }
            cache[key].push(fn);
        };
        var _remove = function (key, cache, fn) {
            if (cache[key]) {
                if (fn) {
                    for (var i = cache[key].length; i >= 0; i--) {
                        if (cache[key] === fn) {
                            cache[key].splice(i, 1);
                        }
                    }
                } else {
                    cache[key] = [];
                }
            }
        };
        var _trigger = function () {
            var self = this;
            var cache = _shift.call(arguments);
            var key = _shift.call(arguments);
            var args = arguments;
            var fnPool = cache[key];
            if (!fnPool || !fnPool.length) {
                return;
            }
            return fnPool.forEach(function (fn) {
                fn.apply(self, args);
            });
        };

        //實作命名空間
        var _create = function (namespace) {
            var namespace = namespace || _default;
            var cache = {},
                offlineStack = [],
                ret = {
                    listen: function (key, fn, last) {
                        _listen(key, fn, cache);
                        if (offlineStack === null) {
                            return;
                        }
                        if (last === 'last') {
                            offlineStack.length && offlineStack.pop()();
                        } else {
                            offlineStack.forEach(function (fn) {
                                fn();
                            });
                        }
                        offlineStack = null;
                    },
                    remove: function (key, fn) {
                        _remove(key, cache, fn);
                    },
                    trigger: function () {
                        var fn, args, self = this;
                        _unshift.call(arguments, cache);
                        args = arguments;
                        fn = function () {
                            return _trigger.apply(self, args);
                        };
                        if (offlineStack) {
                            return offlineStack.push(fn);
                        }
                        return fn();
                    }
                };
            return namespace ? (namespaceCache[namespace] ?
                namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret;
        }

        /*
        回傳實際使用介面,每次使用發佈、訂閱或是刪除其實都是先進到使用命名空間的函數裡,
        取得相對應的命名空間之後,再回傳相對應的函數
        */
        return {
            create: _create,
            remove: function (key, fn) {
                var event = this.create();
                event.remove(key, fn);
            },
            listen: function (key, fn, last) {
                var event = this.create();
                event.listen(key, fn, last);
            },
            trigger: function () {
                var event = this.create();
                event.trigger.apply(this, arguments);
            }
        }
    }()
    return Event;
})();

那實際使用像這樣:

//可以先發佈在訂閱一樣可以收到
Event.create('company1').trigger('test', 1);
Event.create('company1').listen('test', function (a) {
    console.log(a);
});

以上就是發佈/訂閱模式啦


上一篇
JS Design Pattern Day05-發佈/訂閱模式 Publish/Subscribe(上)
下一篇
JS Design Pattern Day07-命令模式 Command
系列文
JS Design Pattern 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言